package org.jetbrains.tinygoplugin.sdk

import com.goide.GoNotifications
import com.goide.GoOsManager
import com.goide.sdk.GoSdk
import com.goide.util.GoUtil
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.NioFiles
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.platform.templates.github.DownloadUtil
import com.intellij.platform.templates.github.ZipUtil
import com.intellij.refactoring.RefactoringBundle
import com.intellij.util.PathUtil
import com.intellij.util.io.Decompressor
import com.intellij.util.io.HttpRequests
import org.jetbrains.tinygoplugin.configuration.TinyGoSdkList
import java.io.IOException
import java.io.UncheckedIOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.Collections
import java.util.function.Consumer

@Service
@Suppress("NestedBlockDepth")
class TinyGoDownloadSdkService private constructor() {
    companion object {
        val logger: Logger = logger<TinyGoDownloadSdkService>()
    }

    val downloadingTinyGoSdks: MutableSet<TinyGoDownloadingSdk> = mutableSetOf()

    fun downloadTinyGoSdk(sdk: TinyGoDownloadingSdk, onFinish: Consumer<TinyGoSdk>) {
        logger.debug("Download of TinyGo SDK started")
        if (sdk.isDownloaded) {
            logger.debug("TinyGo SDK has been already downloaded, exit")
            return
        }
        logger.debug("Waiting for TinyGo SDK to be registered")
        val registered = synchronized(downloadingTinyGoSdks) {
            downloadingTinyGoSdks.add(sdk)
        }
        if (registered) {
            logger.debug("TinyGo SDK registered")
            startDownloading(sdk, onFinish)
        }
    }

    private fun startDownloading(sdk: TinyGoDownloadingSdk, onFinish: Consumer<TinyGoSdk>) {

        val downloadTask: Task.Backgroundable = object : Task.Backgroundable(null, "Downloading TinyGo SDK", true) {
            private var success = false
            private val lock = object {}

            override fun onFinished() {
                logger.debug("Start processing downloaded temporary TinyGo SDK object")
                synchronized(downloadingTinyGoSdks) {
                    downloadingTinyGoSdks.remove(sdk)
                    logger.debug("Removed TinyGo SDK from the list of downloading SDKs")
                }

                val localSdk = sdk.toLocalTinyGoSdk()
                if (localSdk == GoSdk::NULL) {
                    logger.debug("Converting of downloading SDK to local failed")
                    return
                }
                service<TinyGoSdkList>().addSdk(localSdk)
                logger.debug("Added downloaded TinyGo SDK to the list of local SDKs")
                sdk.isDownloaded = true
                synchronized(lock) {
                    if (success) {
                        sdk.isDownloaded = true
                        GoNotifications.getGeneralGroup()
                            .createNotification("Downloaded SDK", NotificationType.INFORMATION)
                            .notify(null)
                    }
                }
                onFinish.accept(localSdk)
            }

            override fun run(indicator: ProgressIndicator) {
                try {
                    logger.debug("Started downloading TinyGo SDK")
                    indicator.isIndeterminate = false
                    val extension = if (GoOsManager.isWindows()) ".zip" else ".tar.gz"
                    var arch = GoUtil.systemArch()
                    val fileName = LazyString { "tinygo${sdk.version}.${GoUtil.systemOS()}-$arch$extension" }
                    val url = LazyString {
                        "https://github.com/$TINYGO_GITHUB/releases/download/v${sdk.version}/$fileName"
                    }
                    @Suppress("SwallowedException")
                    try {
                        HttpRequests.request(url.toString()).tryConnect()
                    } catch (e: IOException) {
                        arch = osManager.emulatedArch(arch)
                    }

                    val downloadedArchive = Files.createTempFile("for-actual-downloading-", extension)
                    DownloadUtil.downloadContentToFile(
                        indicator,
                        url.toString(),
                        downloadedArchive.toFile()
                    )
                    logger.debug("Downloaded TinyGo SDK into temp")
                    indicator.text2 = ""
                    // checksum verifying??
                    unpackSdk(indicator, downloadedArchive, VfsUtilCore.urlToPath(sdk.homeUrl))
                    synchronized(lock) {
                        success = true
                        logger.debug("TinyGo SDK download and unpacking succeeded")
                    }
                } catch (e: IOException) {
                    error("Unable to download TinyGo SDK from GitHub", e, null, onFinish)
                    logger.error(e.message)
                }
            }

            private inner class LazyString(private val supplier: () -> String) {
                override fun toString(): String {
                    return supplier.invoke()
                }
            }

            private fun unpackSdk(indicator: ProgressIndicator, archive: Path, targetPath: String) {
                logger.debug("Start unpacking TinyGo SDK into $targetPath")
                indicator.isIndeterminate = false
                indicator.text = "Unpacking"
                try {
                    val tempDirectory = Files.createTempDirectory("downloaded-sdk-")
                    if (PathUtil.getFileExtension(archive.fileName.toString()).equals("zip")) {
                        ZipUtil.unzip(
                            indicator, tempDirectory.toFile(),
                            archive.toFile(), null,
                            null, false
                        )
                    } else {
                        Decompressor.Tar(archive).extract(tempDirectory)
                    }
                    val tinyGo = tempDirectory.resolve("tinygo")
                    if (!Files.exists(tinyGo) || !Files.isDirectory(tinyGo)) {
                        error("Could not find tinygo directory in downloaded directory", null, null, onFinish)
                    }
                    val targetDir = NioFiles.createDirectories(Paths.get(targetPath))
                    copyDir(tinyGo, targetDir, indicator)
                    LocalFileSystem.getInstance().refreshNioFiles(Collections.singleton(targetDir))
                    FileUtil.asyncDelete(tinyGo.toFile())
                    logger.debug("Unpacked TinyGo SDK into $targetPath")
                } catch (e: IOException) {
                    error("Error unpacking TinyGoSDK", e, null, onFinish)
                }
            }

            private fun copyDir(from: Path, to: Path, indicator: ProgressIndicator) {
                try {
                    Files.walk(from).use { filesStream ->
                        filesStream.forEach { f ->
                            try {
                                indicator.checkCanceled()
                                val targetPath = to.resolve(from.relativize(f))
                                Files.copy(
                                    f,
                                    targetPath,
                                    StandardCopyOption.REPLACE_EXISTING,
                                    StandardCopyOption.COPY_ATTRIBUTES
                                )
                            } catch (e: IOException) {
                                throw UncheckedIOException(e.message, e)
                            }
                        }
                    }
                } catch (e: UncheckedIOException) {
                    error("Error unpacking TinyGoSdk", e, null, onFinish)
                }
            }

            private fun error(message: String, e: Exception?, details: String?, onFinish: Consumer<TinyGoSdk>) {
                GoNotifications.getGeneralGroup().createNotification(
                    "Failed to download TinyGo SDK",
                    message,
                    NotificationType.ERROR
                ).addAction(object : NotificationAction(RefactoringBundle.message("retry.command")) {
                    override fun actionPerformed(e: AnActionEvent, notification: Notification) {
                        notification.expire()
                        if (!sdk.isDownloaded) {
                            this@TinyGoDownloadSdkService.downloadTinyGoSdk(sdk, onFinish)
                        }
                    }
                }).notify(null)
                logger.info(message + ". " + StringUtil.notNullize(details), e)
            }
        }

        logger.debug("Waiting for TinyGo SDK downloading and unpacking task")
        ProgressManager.getInstance().runProcessWithProgressAsynchronously(
            downloadTask, BackgroundableProcessIndicator(downloadTask)
        )
        logger.debug("TinyGo SDK downloading and unpacking task finished")
    }
}
